#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ubuntu 24.04 镜像同步 + HTTP 服务  单文件版
合并：server.py / sync.py / Ubuntu_24.04_Mirrors_Server.py
增加：HTTP 日志重定向到文件，解决 -w 打包后服务异常
"""
import sys, os, time, pathlib, configparser, http.server, socketserver, argparse
import re, lzma, shutil, requests
from pathlib import Path
from multiprocessing.pool import ThreadPool
from tqdm import tqdm
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QThread, pyqtSignal, QTimer, Qt
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QStyle
from functools import lru_cache

__all__ = ["resource_path"]

@lru_cache(maxsize=None)
def resource_path(relative_path: str) -> str:
    if os.path.isabs(relative_path):
        return os.path.normpath(relative_path)
    if hasattr(sys, "_MEIPASS"):
        base = Path(sys._MEIPASS).parent
    else:
        base = Path(__file__).resolve().parent
        if base.name.endswith((".dist", ".build", "_internal")):
            base = base.parent
    return str((base / relative_path).resolve())

CONFIG_FILE = Path(resource_path('config.ini'))
ICO_FILE   = Path(resource_path('Heart.ico'))

# ---------- 配置 ----------
def ensure_schedule(c: configparser.ConfigParser):
    if 'schedule' not in c:
        c.add_section('schedule')
    for k, v in (('enabled', '0'), ('hours', '24'), ('next_ts', '0')):
        if k not in c['schedule']:
            c.set('schedule', k, v)

def load_config():
    c = configparser.ConfigParser()
    if CONFIG_FILE.exists():
        c.read(CONFIG_FILE, encoding='utf-8')
    else:
        c.read_dict({
            'common': {'work_dir': '', 'ubuntu_dist': 'noble',
                       'base_url': 'https://mirrors.aliyun.com/ubuntu'},
            'sync': {'pool_threads': '16'},
            'server': {'port': '8000', 'interface': '0.0.0.0'}
        })
    ensure_schedule(c)
    return c

def save_config(c: configparser.ConfigParser):
    with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
        c.write(f)

# ---------- 同步 ----------
def start_sync(cfg, log):
    WORK_DIR   = Path(cfg['common']['work_dir'])
    UBUNTU_DIST= cfg['common']['ubuntu_dist']
    BASE_URL   = cfg['common']['base_url']
    POOL_THREADS=int(cfg['sync']['pool_threads'])

    def mkdir(p: Path):
        p.mkdir(parents=True, exist_ok=True)

    def download(url: str, dst: Path):
        if dst.exists():
            return
        dst_tmp = dst.with_suffix('.tmp')
        headers = {}
        if dst_tmp.exists():
            headers['Range'] = f'bytes={dst_tmp.stat().st_size}-'
        try:
            with requests.get(url, headers=headers, stream=True, timeout=60) as r:
                if r.status_code == 404:
                    log(f'[WARN] 404 skip: {url}'); return
                r.raise_for_status()
                total = int(r.headers.get('content-length', 0))
                mode = 'ab' if headers else 'wb'
                with dst_tmp.open(mode) as f, tqdm(total=total, unit='B', unit_scale=True, desc=dst.name) as bar:
                    for chunk in r.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk); bar.update(len(chunk))
            dst_tmp.rename(dst)
        except Exception as e:
            log(f'[ERROR] 下载失败 {url} : {e}')

    def sync_release():
        log('== 同步 Release 索引')
        for f in ('Release', 'Release.gpg', 'InRelease'):
            url  = f'{BASE_URL}/dists/{UBUNTU_DIST}/{f}'
            dst  = WORK_DIR / 'dists' / UBUNTU_DIST / f
            mkdir(dst.parent); download(url, dst)

    def parse_packages():
        pkg_xz_url = f'{BASE_URL}/dists/{UBUNTU_DIST}/main/binary-amd64/Packages.xz'
        local_xz   = WORK_DIR / 'dists' / UBUNTU_DIST / 'main' / 'binary-amd64' / 'Packages.xz'
        mkdir(local_xz.parent); download(pkg_xz_url, local_xz)
        local_txt = local_xz.with_suffix('')
        with lzma.open(local_xz) as f_in, local_txt.open('wb') as f_out:
            shutil.copyfileobj(f_in, f_out)
        with local_txt.open('r', encoding='utf-8') as f:
            return re.findall(r'^Filename: (.+)$', f.read(), re.M)

    def sync_debs(filenames):
        log(f'== 共 {len(filenames)} 个 deb 包待同步')
        def fetch(rel: str):
            url = f'{BASE_URL}/{rel}'
            dst = WORK_DIR / rel
            mkdir(dst.parent); download(url, dst)
        with ThreadPool(POOL_THREADS) as pool:
            pool.map(fetch, filenames)
        log('✅ 同步完成！')

    sync_release()
    files = parse_packages()
    sync_debs(files)

# ---------- HTTP 服务（日志重定向版） ----------
def start_server(cfg, log_emit, thread_obj=None):
    WORK_DIR = Path(cfg['common']['work_dir'])
    PORT     = int(cfg['server']['port'])
    IFACE    = cfg['server']['interface']

    # 1. 日志文件
    log_path = WORK_DIR / 'mirror_http.log'
    try:
        log_file = open(log_path, 'a', buffering=1, encoding='utf-8')
    except Exception as e:
        log_emit(f'无法创建日志文件 {log_path}：{e}')
        return

    # 2. 重定向 stdout/stderr → 日志文件（解决 -w 模式句柄无效）
    old_out, old_err = sys.stdout, sys.stderr
    sys.stdout = sys.stderr = log_file

    class StaticHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, directory=str(WORK_DIR), **kwargs)

    log_emit(f'HTTP 日志已重定向 → {log_path}')
    log_emit(f'HTTP 服务根目录: {WORK_DIR}')
    with socketserver.TCPServer((IFACE, PORT), StaticHTTPRequestHandler) as httpd:
        log_emit(f'Serving Ubuntu mirror at http://{IFACE}:{PORT}')
        if thread_obj:
            thread_obj.httpd = httpd
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            pass
        log_emit('HTTP 服务已停止.')

    # 恢复
    sys.stdout, sys.stderr = old_out, old_err
    log_file.close()

# ---------- 后台线程 ----------
class SyncThread(QThread):
    log = pyqtSignal(str)
    def __init__(self, cfg):
        super().__init__(); self.cfg = cfg
    def run(self):
        start_sync(self.cfg, self.log.emit)

class ServerThread(QThread):
    log = pyqtSignal(str)
    def __init__(self, cfg):
        super().__init__(); self.cfg = cfg; self.httpd = None
    def run(self):
        start_server(self.cfg, self.log.emit, self)
    def stop(self):
        if self.httpd:
            self.log.emit('正在关闭 HTTP 服务 …')
            self.httpd.shutdown()
            self.httpd.server_close()

# ---------- 主界面 ----------
class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.cfg = load_config()
        self.sync_thread = None
        self.server_thread = None
        self._build_ui()
        self._build_tray()
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.check_schedule)
        self.timer.start(60_000)
        self.countdown_timer = QTimer(self)
        self.countdown_timer.timeout.connect(self.update_countdown_label)
        self.countdown_timer.start(1000)
        self.reset_next_time()

    # ---------------- UI ----------------
    def _build_ui(self):
        self.setWindowTitle('Ubuntu 镜像同步/服务控制台')
        tabs = QTabWidget()

        # 1. 基本设置
        com = QWidget()
        self.work_dir_edit = QLineEdit(self.cfg['common']['work_dir'])
        btn_browse = QPushButton('浏览...')
        btn_browse.clicked.connect(self.choose_work_dir)
        self.dist_edit = QLineEdit(self.cfg['common']['ubuntu_dist'])
        self.base_url_edit = QLineEdit(self.cfg['common']['base_url'])
        lay = QVBoxLayout()
        for label, w in [('本地镜像根目录', self.work_dir_edit),
                         ('Ubuntu 版本代号', self.dist_edit),
                         ('上游镜像站 URL', self.base_url_edit)]:
            h = QHBoxLayout()
            h.addWidget(QLabel(label))
            h.addWidget(w)
            lay.addLayout(h)
        h = QHBoxLayout()
        h.addWidget(self.work_dir_edit)
        h.addWidget(btn_browse)
        lay.insertLayout(1, h)
        com.setLayout(lay)
        tabs.addTab(com, '基本设置')

        # 2. 镜像同步
        sync = QWidget()
        self.threads_spin = QSpinBox()
        self.threads_spin.setRange(1, 64)
        self.threads_spin.setValue(int(self.cfg['sync']['pool_threads']))
        self.log_sync = QTextEdit()
        self.btn_sync = QPushButton('开始同步')
        self.btn_sync.clicked.connect(self.start_sync)
        lay = QVBoxLayout()
        h = QHBoxLayout()
        h.addWidget(QLabel('并行线程数'))
        h.addWidget(self.threads_spin)
        h.addStretch()
        lay.addLayout(h)
        lay.addWidget(self.btn_sync)
        lay.addWidget(self.log_sync)
        sync.setLayout(lay)
        tabs.addTab(sync, '镜像同步')

        # 3. 本地服务
        serv = QWidget()
        self.port_spin = QSpinBox()
        self.port_spin.setRange(1024, 65535)
        self.port_spin.setValue(int(self.cfg['server']['port']))
        self.iface_edit = QLineEdit(self.cfg['server']['interface'])
        self.log_serv = QTextEdit()
        self.btn_serv_start = QPushButton('启动 HTTP 服务')
        self.btn_serv_stop  = QPushButton('停止 HTTP 服务')
        self.btn_serv_start.clicked.connect(self.start_server)
        self.btn_serv_stop.clicked.connect(self.stop_server)

        lay = QVBoxLayout()
        h = QHBoxLayout()
        h.addWidget(QLabel('监听端口'))
        h.addWidget(self.port_spin)
        h.addWidget(QLabel('绑定地址'))
        h.addWidget(self.iface_edit)
        lay.addLayout(h)

        h2 = QHBoxLayout()
        h2.addWidget(self.btn_serv_start)
        h2.addWidget(self.btn_serv_stop)
        h2.addStretch()
        lay.addLayout(h2)
        lay.addWidget(self.log_serv)
        serv.setLayout(lay)
        tabs.addTab(serv, '本地服务')

        # 4. 定时同步
        sched = QWidget()
        self.enable_chk = QCheckBox('启用定时同步')
        self.enable_chk.setChecked(int(self.cfg['schedule']['enabled']))
        self.enable_chk.stateChanged.connect(self.reset_next_time)
        self.hours_spin = QSpinBox()
        self.hours_spin.setRange(1, 168)
        self.hours_spin.setValue(int(self.cfg['schedule']['hours']))
        self.hours_spin.valueChanged.connect(self.reset_next_time)
        self.countdown_label = QLabel()
        self.btn_reset = QPushButton('重置倒计时')
        self.btn_reset.clicked.connect(self.reset_next_time)
        lay = QVBoxLayout()
        h = QHBoxLayout()
        h.addWidget(self.enable_chk)
        h.addWidget(QLabel('间隔（小时）'))
        h.addWidget(self.hours_spin)
        h.addStretch()
        lay.addLayout(h)
        lay.addWidget(self.countdown_label)
        lay.addWidget(self.btn_reset)
        lay.addStretch()
        sched.setLayout(lay)
        tabs.addTab(sched, '定时同步')

        main = QVBoxLayout(self)
        main.addWidget(tabs)
        self.resize(700, 500)

    # ---------------- 托盘 ----------------
    def _build_tray(self):
        if ICO_FILE.exists():
            self.tray_icon = QSystemTrayIcon(QIcon(str(ICO_FILE)), self)
        else:
            self.tray_icon = QSystemTrayIcon(self.style().standardIcon(QStyle.SP_ComputerIcon), self)
        self.tray_icon.setToolTip('Ubuntu Mirror 控制台')
        menu = QMenu()
        show_action = QAction('显示窗口', self)
        show_action.triggered.connect(self.show_normal)
        quit_action = QAction('退出', self)
        quit_action.triggered.connect(qApp.quit)
        menu.addAction(show_action)
        menu.addSeparator()
        menu.addAction(quit_action)
        self.tray_icon.setContextMenu(menu)
        self.tray_icon.activated.connect(self._tray_activated)
        self.tray_icon.show()

    def _tray_activated(self, reason):
        if reason == QSystemTrayIcon.DoubleClick:
            self.show_normal()

    def show_normal(self):
        self.show(); self.raise_(); self.activateWindow(); self.tray_icon.hide()

    def closeEvent(self, event):
        event.ignore(); self.hide(); self.tray_icon.show()

    # ---------------- 逻辑 ----------------
    def choose_work_dir(self):
        d = QFileDialog.getExistingDirectory(self, '选择镜像根目录')
        if d:
            self.work_dir_edit.setText(d)

    def gather_cfg(self):
        self.cfg['common']['work_dir']   = self.work_dir_edit.text()
        self.cfg['common']['ubuntu_dist']= self.dist_edit.text()
        self.cfg['common']['base_url']   = self.base_url_edit.text()
        self.cfg['sync']['pool_threads'] = str(self.threads_spin.value())
        self.cfg['server']['port']       = str(self.port_spin.value())
        self.cfg['server']['interface']  = self.iface_edit.text()
        self.cfg['schedule']['enabled']  = str(int(self.enable_chk.isChecked()))
        self.cfg['schedule']['hours']    = str(self.hours_spin.value())
        save_config(self.cfg)
        return self.cfg

    def start_sync(self):
        if self.sync_thread and self.sync_thread.isRunning():
            QMessageBox.information(self, '提示', '同步任务进行中...'); return
        self.log_sync.clear()
        self.sync_thread = SyncThread(self.gather_cfg())
        self.sync_thread.log.connect(self.log_sync.append)
        self.sync_thread.start()
        self.sync_thread.finished.connect(self.reset_next_time)

    def start_server(self):
        if self.server_thread is None or not self.server_thread.isRunning():
            self.log_serv.clear()
            self.server_thread = ServerThread(self.gather_cfg())
            self.server_thread.log.connect(self.log_serv.append)
            self.server_thread.start()

    def stop_server(self):
        if self.server_thread and self.server_thread.isRunning():
            self.server_thread.stop()
            self.server_thread.wait(2000)
            self.log_serv.append('HTTP 服务已停止.')
        else:
            self.log_serv.append('HTTP 服务未运行.')

    # ---------------- 定时 ----------------
    def reset_next_time(self):
        self.cfg['schedule']['enabled'] = str(int(self.enable_chk.isChecked()))
        self.cfg['schedule']['hours']   = str(self.hours_spin.value())
        save_config(self.cfg)
        if self.enable_chk.isChecked():
            self.countdown_timer.start(1000)
            hours   = int(self.cfg['schedule']['hours'])
            next_ts = int(time.time()) + hours * 3600
            self.cfg['schedule']['next_ts'] = str(next_ts)
            save_config(self.cfg)
        else:
            self.countdown_timer.stop()
            self.countdown_label.setText('定时同步已关闭')

    def update_countdown_label(self):
        if not self.enable_chk.isChecked():
            self.countdown_label.setText('定时同步已关闭'); return
        next_ts = int(self.cfg.get('schedule', 'next_ts', fallback=0))
        now = int(time.time())
        left = next_ts - now
        if left <= 0:
            self.countdown_label.setText('即将触发同步...'); return
        h, rem = divmod(left, 3600)
        m, s   = divmod(rem, 60)
        self.countdown_label.setText(f'距离下次同步：{h:02d}:{m:02d}:{s:02d}')

    def check_schedule(self):
        self.update_countdown_label()
        if not self.enable_chk.isChecked():
            return
        next_ts = int(self.cfg.get('schedule', 'next_ts', fallback=0))
        if next_ts != 0 and time.time() >= next_ts:
            self.reset_next_time()
            if not (self.sync_thread and self.sync_thread.isRunning()):
                self.start_sync()

# ---------- 启动 ----------
if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setQuitOnLastWindowClosed(False)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())
